media_pp\elements\driver\webrtc/peer.rs
1use std::{
2 collections::HashMap,
3 net::UdpSocket,
4 sync::{
5 Arc, Mutex,
6 atomic::{AtomicU64, Ordering},
7 },
8 time::{Duration, Instant},
9};
10
11use crate::pp_log::{PpLog, pp_error, pp_info};
12use crossbeam_channel::{Receiver, Sender, TrySendError, bounded, unbounded};
13use ffmpeg_next as ffmpeg;
14use str0m::{
15 Event, Input, Output, Rtc,
16 change::{SdpOffer, SdpPendingOffer},
17 format::Codec,
18 media::{MediaKind, MediaTime, Mid},
19 net::{Protocol, Receive},
20};
21
22use crate::{
23 buffer::MediaBuffer,
24 bus::{Bus, BusEvent},
25 driver::{Driver, StopReceiver},
26 element::{Element, ElementType, element_pp_log},
27 error::Result,
28 time::{InvalidTimeBase, MediaTimestamp},
29};
30
31use super::{
32 command::{Command, TrackId, TrackOutState, WebRtcError},
33 track::{WebRtcHandle, WebRtcTrackSink, WebRtcTrackSource},
34};
35
36/// How often `WebRtcPeer::run` re-checks `stop`/its command channel while
37/// otherwise blocked on the UDP socket — see its own docs for why this is
38/// polling rather than a true multi-way wait.
39const POLL_INTERVAL: Duration = Duration::from_millis(20);
40
41/// Bound on the command channel (see [`Command`]) and on each attached
42/// track's inbound buffer (`WebRtcPeer` -> its `WebRtcTrackSource`). Once
43/// this many media buffers are backed up, the newest one is dropped
44/// instead of piling up in memory forever — the right call for live media,
45/// where a backed-up peer means falling behind, not something worth
46/// buffering indefinitely for (same reasoning as
47/// [`crate::queue::OverflowPolicy::DropNewest`]). Control traffic on the
48/// same channel (`AddTrack`/`SetAnswer`/`AcceptOffer`) is never dropped for
49/// capacity pressure — those call sites block on plain `send` instead of
50/// `try_send`. A disconnected peer still rejects the command; `set_answer`
51/// intentionally treats that case as a no-op.
52const CHANNEL_CAPACITY: usize = 128;
53/// The [`Driver`] — owns the [`Rtc`] session and its [`UdpSocket`], and
54/// drives str0m's sans-I/O poll loop on the dedicated thread
55/// [`crate::driver::DriverRunner::run`] gives it. Not a
56/// [`crate::element::SourceElement`]/[`crate::element::Source`]: it has no `src_pads()`
57/// dataflow graph of its own — see [`Driver`]'s own docs for why a
58/// connection with dynamically-appearing, independently bidirectional
59/// tracks doesn't fit that shape. Whatever it produces or consumes flows
60/// through the separate [`WebRtcTrackSink`]/[`WebRtcTrackSource`] pairs it
61/// mints per track instead (see below).
62///
63/// `rtc`/`socket` must already be connected: the initial SDP offer/answer
64/// and ICE candidate setup happen via str0m directly, in the caller's own
65/// code, *before* [`WebRtcPeer::new`]. `WebRtcPeer` only takes over after
66/// signaling has established the connection; it does not provide a signaling
67/// server itself.
68///
69/// Every track — whether it's one this side requested via
70/// [`WebRtcHandle::add_track`] or one the remote peer added (`str0m`'s
71/// `Event::MediaAdded`, which — critically — *never fires for a track this
72/// side added itself*) — is attached the same way, the moment its `Mid`
73/// exists: a [`WebRtcTrackSink`] (to reply on) and a [`WebRtcTrackSource`]
74/// (whatever the remote side sends on it) are minted together and handed
75/// out through [`WebRtcHandle::next_track`], no closure required. A single
76/// `Direction::SendRecv` track therefore needs exactly one
77/// [`WebRtcHandle::add_track`] call (on either side) and one
78/// `next_track()` on *each* side — no separate outbound API, and no
79/// special-casing for which side happened to originate it.
80/// (`Direction::SendOnly`/`RecvOnly` still work the same way; the unused
81/// half of the pair — a `WebRtcTrackSource` nothing ever sends on, or a
82/// `WebRtcTrackSink` str0m has no send capability for — is simply inert,
83/// not an error.) This is the same idea as
84/// [`crate::elements::TeeHandle::attach`]'s dynamic attachment, just
85/// without `Tee`'s `Mutex` (nothing but this one thread ever touches
86/// `tracks_in`).
87pub struct WebRtcPeer {
88 pp_log: PpLog,
89 name: Arc<str>,
90 rtc: Rtc,
91 socket: UdpSocket,
92 /// Where inbound data for each attached track goes: just a plain
93 /// `Sender`, not a `Box<dyn Sink>` — the matching `Receiver` lives
94 /// inside that track's own [`WebRtcTrackSource`], driven by *its own*
95 /// `Pipeline` on its own thread, so nothing here needs to know about
96 /// `ControlMsg` at all. The `Mutex` alongside it is the same
97 /// `WebRtcTrackSource`'s [`WebRtcTrackSource::codec`] cell — written
98 /// here (from `Event::MediaData`), read there, from whatever thread the
99 /// caller checks it on. It's the one piece of `tracks_in` shared across
100 /// threads; the map itself still isn't (see below).
101 #[allow(clippy::type_complexity)]
102 tracks_in: HashMap<Mid, (Sender<MediaBuffer>, Arc<Mutex<Option<Codec>>>)>,
103 tracks_out: HashMap<TrackId, TrackOutState>,
104 /// The codec each `tracks_out` entry was opened with — what
105 /// [`WebRtcHandle::add_track`] was told to expect, consulted by
106 /// `write_track` to pick the payload type matching what's actually
107 /// being pushed instead of guessing at whatever this connection
108 /// happened to negotiate first for that `Mid`.
109 track_codec: HashMap<TrackId, Codec>,
110 /// The one SDP exchange currently in flight (str0m only allows one at a
111 /// time — see `chat.rs`'s own `pending.is_some()` guard), plus which
112 /// `TrackId`s it covers, so [`Command::SetAnswer`] knows which entries
113 /// in `tracks_out` to flip from `Negotiating` to `Open`.
114 pending: Option<(SdpPendingOffer, Vec<TrackId>)>,
115 /// Shared with every [`WebRtcHandle`] clone, so `TrackId`s minted here
116 /// (for tracks the *remote* peer added — see the type docs) never
117 /// collide with ones `WebRtcHandle::add_track` mints.
118 next_id: Arc<AtomicU64>,
119 /// Cloned into every [`WebRtcTrackSink`] this element hands out via
120 /// [`WebRtcPeer::attach_track`] — including for tracks *this* side
121 /// requested, since `WebRtcTrackSink` is otherwise only ever
122 /// constructed from inside `run`.
123 command_tx: Sender<Command>,
124 command_rx: Receiver<Command>,
125 /// The other half of [`WebRtcHandle::next_track`] — one entry per
126 /// newly-attached track, in attachment order (see [`TrackId`]'s own
127 /// docs for why the caller has to match on it, not just take these in
128 /// order, when more than one track can appear).
129 new_track_tx: Sender<(TrackId, Mid, MediaKind, WebRtcTrackSink, WebRtcTrackSource)>,
130 on_offer: Box<dyn FnMut(SdpOffer) + Send>,
131 on_keyframe_request: Box<dyn FnMut(TrackId) + Send>,
132}
133impl WebRtcPeer {
134 /// `rtc`/`socket` must already be connected — see the type-level docs.
135 /// `on_offer` receives every renegotiation offer this element generates
136 /// (via [`WebRtcHandle::add_track`]) for the caller to ship over its
137 /// own signaling transport; `on_keyframe_request` reports which
138 /// outbound track the remote peer wants a keyframe for (forward this to
139 /// whatever's encoding that track). Newly-attached tracks themselves
140 /// come from [`WebRtcHandle::next_track`], not a constructor argument.
141 pub fn new(
142 name: impl Into<String>,
143 rtc: Rtc,
144 socket: UdpSocket,
145 on_offer: impl FnMut(SdpOffer) + Send + 'static,
146 on_keyframe_request: impl FnMut(TrackId) + Send + 'static,
147 ) -> (Self, WebRtcHandle) {
148 let name: Arc<str> = name.into().into();
149 let pp_log = element_pp_log(ElementType::WebRtcPeer, &name, None);
150 pp_info!(
151 pp_log: &pp_log,
152 "created: local_addr={:?}",
153 socket.local_addr()
154 );
155 let (command_tx, command_rx) = bounded(CHANNEL_CAPACITY);
156 let (new_track_tx, new_track_rx) = unbounded();
157 let next_id = Arc::new(AtomicU64::new(0));
158 (
159 Self {
160 name,
161 pp_log,
162 rtc,
163 socket,
164 tracks_in: HashMap::new(),
165 tracks_out: HashMap::new(),
166 track_codec: HashMap::new(),
167 pending: None,
168 next_id: next_id.clone(),
169 command_tx: command_tx.clone(),
170 command_rx,
171 new_track_tx,
172 on_offer: Box::new(on_offer),
173 on_keyframe_request: Box::new(on_keyframe_request),
174 },
175 WebRtcHandle {
176 next_id,
177 command_tx,
178 new_track_rx,
179 },
180 )
181 }
182
183 /// Mints a fresh [`WebRtcTrackSink`]/[`WebRtcTrackSource`] pair for
184 /// `mid`/`kind` and hands both out via [`WebRtcHandle::next_track`] —
185 /// see the type docs for why this is the one path both locally- and
186 /// remotely-added tracks go through.
187 fn attach_track(&mut self, id: TrackId, mid: Mid, kind: MediaKind) {
188 pp_info!(self, "track attached: id={id:?}, mid={mid}, kind={kind:?}");
189 let reply = WebRtcTrackSink::new(id, self.command_tx.clone());
190 let (tx, rx) = bounded(CHANNEL_CAPACITY);
191 let codec = Arc::new(Mutex::new(None));
192 self.tracks_in.insert(mid, (tx, codec.clone()));
193 let source = WebRtcTrackSource::new(format!("webrtc-track-{}-in", id.0), rx, codec);
194 let _ = self.new_track_tx.send((id, mid, kind, reply, source));
195 }
196
197 fn apply_command(&mut self, cmd: Command, bus: &Bus) -> Result<()> {
198 match cmd {
199 Command::AddTrack(id, kind, direction, codec) => {
200 pp_info!(
201 self,
202 "add_track requested: id={id:?}, kind={kind:?}, direction={direction:?}, codec={codec:?}"
203 );
204 self.tracks_out
205 .insert(id, TrackOutState::ToOpen(kind, direction));
206 self.track_codec.insert(id, codec);
207 }
208 Command::Push(id, buf) => {
209 // A malformed media packet is local to this one track and
210 // buffer. Report and drop it without tearing down the
211 // entire live WebRTC connection, matching Queue's
212 // consume-error contract.
213 if let Err(error) = self.write_track(id, buf) {
214 bus.post(
215 &self.pp_log,
216 BusEvent::Error {
217 element_type: ElementType::WebRtcPeer,
218 name: self.name.clone(),
219 error,
220 },
221 );
222 }
223 }
224 Command::SetAnswer(answer) => {
225 let Some((pending, ids)) = self.pending.take() else {
226 return Ok(());
227 };
228 self.rtc
229 .sdp_api()
230 .accept_answer(pending, answer)
231 .inspect_err(|error| pp_error!(self, "accept_answer failed: {error}"))
232 .map_err(WebRtcError::from)?;
233 pp_info!(self, "renegotiation complete: {} track(s)", ids.len());
234 for id in ids {
235 if let Some(state @ TrackOutState::Negotiating(_)) = self.tracks_out.get(&id) {
236 let mid = state.mid().expect("Negotiating always carries a Mid");
237 self.tracks_out.insert(id, TrackOutState::Open(mid));
238 }
239 }
240 }
241 Command::AcceptOffer(offer, reply) => {
242 let result = self
243 .rtc
244 .sdp_api()
245 .accept_offer(offer)
246 .inspect_err(|error| pp_error!(self, "accept_offer failed: {error}"))
247 .map_err(WebRtcError::from);
248 if result.is_ok() {
249 pp_info!(self, "accepted remote offer");
250 }
251 let _ = reply.send(result);
252 }
253 }
254 Ok(())
255 }
256
257 /// Starts a new SDP exchange if any track is waiting to be opened and
258 /// none is already in flight (str0m only allows one pending offer at a
259 /// time).
260 fn negotiate_if_needed(&mut self) {
261 if self.pending.is_some() {
262 return;
263 }
264 let to_open: Vec<TrackId> = self
265 .tracks_out
266 .iter()
267 .filter(|(_, s)| matches!(s, TrackOutState::ToOpen(..)))
268 .map(|(id, _)| *id)
269 .collect();
270 if to_open.is_empty() {
271 return;
272 }
273
274 let mut newly_negotiating = Vec::with_capacity(to_open.len());
275 let mut api = self.rtc.sdp_api();
276 for &id in &to_open {
277 let Some(TrackOutState::ToOpen(kind, direction)) = self.tracks_out.get(&id) else {
278 continue;
279 };
280 let (kind, direction) = (*kind, *direction);
281 let mid = api.add_media(kind, direction, None, None, None);
282 self.tracks_out.insert(id, TrackOutState::Negotiating(mid));
283 newly_negotiating.push((id, mid, kind));
284 }
285
286 if let Some((offer, pending)) = api.apply() {
287 pp_info!(self, "renegotiation started: {} track(s)", to_open.len());
288 self.pending = Some((pending, to_open));
289 (self.on_offer)(offer);
290 }
291
292 // str0m never fires `Event::MediaAdded` for media *this side* just
293 // added (see the type docs) — so this is the only place these
294 // newly-minted `Mid`s ever reach `attach_track`, unlike the remote
295 // side's own `Event::MediaAdded` handling below.
296 for (id, mid, kind) in newly_negotiating {
297 self.attach_track(id, mid, kind);
298 }
299 }
300
301 fn write_track(&mut self, id: TrackId, buf: MediaBuffer) -> Result<()> {
302 let Some(TrackOutState::Open(mid)) = self.tracks_out.get(&id) else {
303 // Not open yet (or unknown/never added) — dropped, see
304 // `WebRtcHandle::add_track`'s docs.
305 return Ok(());
306 };
307 let MediaBuffer::Packet(packet) = buf else {
308 return Ok(()); // Eos: nothing to write, nothing to flush
309 };
310 let Some(writer) = self.rtc.writer(*mid) else {
311 return Ok(());
312 };
313 // Only a locally-`add_track`ed track has a declared codec (the
314 // caller told us what it intends to push — see `add_track`'s
315 // docs). A remotely-added one (`Event::MediaAdded`) has no such
316 // declaration available at attach time, so this falls back to
317 // whatever this connection negotiated first for the `Mid` — same
318 // best-effort guess this always made, just now scoped to only the
319 // case that has no better option.
320 let pt = match self.track_codec.get(&id) {
321 Some(&codec) => writer.payload_params().find(|p| p.spec().codec == codec),
322 None => writer.payload_params().next(),
323 }
324 .map(|p| p.pt());
325 let Some(pt) = pt else {
326 return Ok(()); // no negotiated codec (matching or otherwise) yet
327 };
328 let data = packet.data().unwrap_or(&[]).to_vec();
329 let rtp_time = packet_rtp_time(&packet)?;
330 writer
331 .write(pt, Instant::now(), rtp_time, data)
332 .inspect_err(|error| pp_error!(self, "writer.write failed: {error}"))
333 .map_err(WebRtcError::from)?;
334 Ok(())
335 }
336
337 /// Drains every immediately-available str0m output (retransmits and
338 /// events), returning once str0m itself has nothing left to do until
339 /// the returned deadline.
340 fn drive_until_timeout(&mut self, bus: &Bus) -> Result<Instant> {
341 loop {
342 let output = self
343 .rtc
344 .poll_output()
345 .inspect_err(|error| pp_error!(self, "poll_output failed: {error}"))
346 .map_err(WebRtcError::from)?;
347 match output {
348 Output::Timeout(deadline) => return Ok(deadline),
349 Output::Transmit(t) => {
350 // A single failed send (e.g. transient ICMP unreachable)
351 // isn't fatal to the whole connection — str0m's own
352 // retransmit/timeout logic handles loss.
353 let _ = self.socket.send_to(&t.contents, t.destination);
354 }
355 Output::Event(event) => self.handle_event(event, bus),
356 }
357 }
358 }
359
360 fn handle_event(&mut self, event: Event, bus: &Bus) {
361 match event {
362 Event::MediaAdded(added) => {
363 // Only reached for media the *remote* peer added (see the
364 // type docs) — by definition already fully negotiated by
365 // the time we see this, so `Open` immediately: unlike a
366 // locally-requested track, there's no answer left to wait
367 // for before a `WebRtcTrackSink` bound to it can actually
368 // send.
369 let id = TrackId(self.next_id.fetch_add(1, Ordering::Relaxed));
370 self.tracks_out.insert(id, TrackOutState::Open(added.mid));
371 self.attach_track(id, added.mid, added.kind);
372 }
373 Event::MediaData(data) => {
374 if let Some((tx, codec)) = self.tracks_in.get(&data.mid) {
375 // Every packet, not just the first: cheap (one lock),
376 // and correct if the remote side ever actually changes
377 // codec mid-stream (rare, but the payload type is free
378 // to vary packet-to-packet — see `WebRtcTrackSource::
379 // codec`'s own docs for why this can't be pinned down
380 // any earlier than "whatever the last packet said").
381 *codec.lock().unwrap() = Some(data.params.spec().codec);
382
383 let mut packet = ffmpeg::Packet::copy(&data.data);
384 // `data.time` is str0m's own RTP timestamp (numerator)
385 // over the codec's clock rate (denominator) — reused
386 // as-is for pts/dts. No B-frame reordering happens over
387 // RTP (decode order == transmit order), so pts and dts
388 // are always the same value here.
389 packet.set_time_base(ffmpeg::Rational::new(1, data.time.denom() as i32));
390 let pts = data.time.numer() as i64;
391 packet.set_pts(Some(pts));
392 packet.set_dts(Some(pts));
393 if data.is_keyframe() {
394 let flags = packet.flags() | ffmpeg::codec::packet::Flags::KEY;
395 packet.set_flags(flags);
396 }
397 match tx.try_send(MediaBuffer::Packet(Arc::new(packet))) {
398 Ok(()) => {}
399 Err(TrySendError::Full(_)) => {
400 // This track's `WebRtcTrackSource` (or whatever
401 // it feeds) isn't keeping up — drop the newest
402 // buffer rather than let this grow unbounded
403 // (see `CHANNEL_CAPACITY`'s docs).
404 bus.post(
405 &self.pp_log,
406 BusEvent::Dropped {
407 element_type: ElementType::WebRtcPeer,
408 name: self.name.clone(),
409 },
410 );
411 }
412 Err(TrySendError::Disconnected(_)) => {
413 // This track's `WebRtcTrackSource` is gone (its
414 // own `Pipeline` finished) — stop trying to feed
415 // it.
416 self.tracks_in.remove(&data.mid);
417 }
418 }
419 }
420 }
421 Event::KeyframeRequest(req) => {
422 if let Some((&id, _)) = self
423 .tracks_out
424 .iter()
425 .find(|(_, s)| s.mid() == Some(req.mid))
426 {
427 pp_info!(self, "keyframe requested: id={id:?}, mid={}", req.mid);
428 (self.on_keyframe_request)(id);
429 }
430 }
431 Event::Connected => {
432 pp_info!(self, "ICE+DTLS connected");
433 }
434 Event::IceConnectionStateChange(state) => {
435 pp_info!(self, "ICE connection state: {state:?}");
436 }
437 Event::MediaChanged(changed) => {
438 pp_info!(
439 self,
440 "media changed: mid={}, direction={:?}",
441 changed.mid,
442 changed.direction
443 );
444 }
445 // `Event` is `#[non_exhaustive]` — data channels, stats, etc.
446 // are still outside this element's concern for now.
447 _ => {}
448 }
449 }
450}
451
452/// Converts a `Packet`'s `(pts, time_base)` into the `MediaTime` str0m
453/// expects for [`str0m::media::Writer::write`]. `MediaTime` is
454/// numer/denom *seconds* (str0m rebases it to the codec's RTP clock rate
455/// internally), but an FFmpeg `time_base` is numer/denom *seconds per
456/// tick* — so the elapsed time is `pts * numerator / denominator`, not
457/// `pts / denominator`. Most time bases in this codebase have numerator 1
458/// (e.g. `1/90_000`), which would hide a naive `pts / denominator`: an
459/// NTSC-style `1001/30_000` time base would make the RTP timestamp run
460/// ~1001x too fast.
461pub(super) fn packet_rtp_time(
462 packet: &ffmpeg::Packet,
463) -> std::result::Result<MediaTime, WebRtcError> {
464 let pts = packet.pts().ok_or(WebRtcError::MissingPacketPts)?;
465 let timestamp = MediaTimestamp::try_new(pts, packet.time_base()).map_err(
466 |InvalidTimeBase {
467 numerator,
468 denominator,
469 }| WebRtcError::InvalidPacketTimeBase {
470 numerator,
471 denominator,
472 },
473 )?;
474 to_str0m_media_time(timestamp)
475}
476
477/// Converts a validated `(pts, time_base)` into the `MediaTime` str0m
478/// expects for [`str0m::media::Writer::write`]. `MediaTime` is numer/denom
479/// *seconds* (str0m rebases it to the codec's RTP clock rate internally),
480/// but an FFmpeg `time_base` is numer/denom *seconds per tick* — so the
481/// elapsed time is `pts * numerator / denominator`, not `pts /
482/// denominator`. This keeps that exact `(pts * numerator, denominator)`
483/// rational rather than rescaling to some fixed target base first — a
484/// backend-specific conversion, so it lives here rather than on
485/// `MediaTimestamp` itself.
486fn to_str0m_media_time(timestamp: MediaTimestamp) -> std::result::Result<MediaTime, WebRtcError> {
487 let time_base = timestamp.time_base().get();
488 let numerator = time_base.numerator();
489 let denominator = time_base.denominator();
490 let pts = u64::try_from(timestamp.pts())
491 .map_err(|_| WebRtcError::NegativePacketPts(timestamp.pts()))?;
492 let frequency = str0m::media::Frequency::new(denominator as u32).ok_or(
493 WebRtcError::InvalidPacketTimeBase {
494 numerator,
495 denominator,
496 },
497 )?;
498 let numer = pts
499 .checked_mul(numerator as u64)
500 .ok_or(WebRtcError::PacketTimestampOverflow {
501 pts,
502 numerator,
503 denominator,
504 })?;
505 Ok(MediaTime::new(numer, frequency))
506}
507
508impl Element for WebRtcPeer {
509 fn name(&self) -> Arc<str> {
510 self.name.clone()
511 }
512
513 fn element_type(&self) -> ElementType {
514 ElementType::WebRtcPeer
515 }
516
517 fn pp_log(&self) -> &PpLog {
518 &self.pp_log
519 }
520
521 fn pp_log_mut(&mut self) -> &mut PpLog {
522 &mut self.pp_log
523 }
524}
525
526impl Driver for WebRtcPeer {
527 /// Drives str0m's poll loop. Every iteration: apply any commands from
528 /// `WebRtcHandle`/`WebRtcTrackSink`, start a renegotiation if a track
529 /// is waiting, drain str0m's own output (writing/dispatching as it
530 /// goes), check `stop`, then block on the UDP socket for at most
531 /// `POLL_INTERVAL` — capped below whatever str0m itself asked for, so
532 /// the command channel and `stop` are never starved for longer than
533 /// that even when nothing else is happening. There's no true
534 /// multi-way wait across the command channel, `stop`, *and* a raw
535 /// socket the way [`crate::elements::AppSource`] manages across two
536 /// `crossbeam_channel`s (a `UdpSocket` isn't `select!`-able), so this
537 /// is bounded polling instead — worst case `POLL_INTERVAL` of extra
538 /// latency for `Stop`/a fresh `add_track`, not unboundedly stuck.
539 ///
540 /// `stop`/the connection dying both clear `tracks_in` immediately, so
541 /// every already-handed-out `WebRtcTrackSource` sees its data channel
542 /// disconnect and ends with a final `Eos` right away, instead of
543 /// waiting for this whole `WebRtcPeer` to be dropped later by whatever
544 /// owns its `DriverRunner`. Neither `WebRtcPeer` nor its
545 /// `WebRtcTrackSource`s have a `Pause`/`Seek` concept — see
546 /// [`Driver`]'s own docs for why that's not just an oversight: freezing
547 /// this loop would starve ICE keepalives/DTLS retransmits, likely
548 /// dropping the connection rather than gracefully suspending it.
549 fn run(&mut self, stop: &StopReceiver, bus: &Bus) -> Result<()> {
550 pp_info!(self, "started");
551 let mut buf = vec![0u8; 2000];
552 loop {
553 while let Ok(cmd) = self.command_rx.try_recv() {
554 self.apply_command(cmd, bus)?;
555 }
556 self.negotiate_if_needed();
557
558 let deadline = self.drive_until_timeout(bus)?;
559 if !self.rtc.is_alive() || stop.is_stopped() {
560 pp_info!(self, "stopped rtc_alive={}", self.rtc.is_alive());
561 self.tracks_in.clear();
562 return Ok(());
563 }
564
565 let wait = deadline
566 .saturating_duration_since(Instant::now())
567 .min(POLL_INTERVAL)
568 .max(Duration::from_millis(1));
569 self.socket
570 .set_read_timeout(Some(wait))
571 .inspect_err(|error| pp_error!(self, "set_read_timeout failed: {error}"))
572 .map_err(WebRtcError::from)?;
573
574 match self.socket.recv_from(&mut buf) {
575 Ok((n, source)) => {
576 let Ok(contents) = buf[..n].try_into() else {
577 continue; // not a WebRTC datagram we recognize — ignore
578 };
579 let destination = self
580 .socket
581 .local_addr()
582 .inspect_err(|error| pp_error!(self, "local_addr failed: {error}"))
583 .map_err(WebRtcError::from)?;
584 self.rtc
585 .handle_input(Input::Receive(
586 Instant::now(),
587 Receive {
588 proto: Protocol::Udp,
589 source,
590 destination,
591 contents,
592 },
593 ))
594 .inspect_err(|error| {
595 pp_error!(self, "handle_input(Receive) failed: {error}")
596 })
597 .map_err(WebRtcError::from)?;
598 }
599 Err(e)
600 if matches!(
601 e.kind(),
602 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
603 ) =>
604 {
605 // Nothing arrived, but str0m still needs to be told
606 // time has passed — its own internal clock only moves
607 // forward via `Input::Timeout`, and *that* is what
608 // makes the next `poll_output()` produce whatever's
609 // next (retransmits, RTCP, the initial STUN checks,
610 // ...). Skipping this on every timeout would leave
611 // str0m stuck forever waiting for input that already
612 // isn't coming.
613 self.rtc
614 .handle_input(Input::Timeout(Instant::now()))
615 .inspect_err(|error| {
616 pp_error!(self, "handle_input(Timeout) failed: {error}")
617 })
618 .map_err(WebRtcError::from)?;
619 }
620 Err(e) => {
621 pp_error!(self, "recv_from failed: {e}");
622 return Err(WebRtcError::from(e).into());
623 }
624 }
625 }
626 }
627}